package com.sangmo.fts.common;

import com.sangmo.boot.framework.common.utils.ApplicationUtils;
import com.sangmo.boot.framework.data.filter.Criteria;
import com.sangmo.boot.framework.data.filter.PageResult;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.jpa.domain.Specification;

import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Expression;
import javax.persistence.criteria.Join;
import javax.persistence.criteria.JoinType;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import javax.persistence.criteria.Selection;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

public class QueryBuilder {
    public static QueryBuilder newInstance() {
        return new QueryBuilder();
    }

    public QueryBuilder() {
        em = ApplicationUtils.getBean(EntityManager.class);
        cb = em.getCriteriaBuilder();
    }

    private EntityManager em;
    private Class domainClass;
    private CriteriaBuilder cb;
    private CriteriaQuery cq;
    private Root root;
    private Map<String, Join> joinMap = new HashMap<>();
    private Join currentJoin;

    public QueryBuilder query1(Class clazz) {
        cq = cb.createQuery(clazz);
        return this;
    }

    public QueryBuilder from2(Class clazz) {
        root = cq.from(clazz);
        domainClass = clazz;
        return this;
    }

    public QueryBuilder join3(String property, JoinType type) {
        Join join = root.join(property, type);
        joinMap.put(property, join);
        currentJoin = join;
        return this;
    }

    public QueryBuilder on4(Predicate... predicates) {
        currentJoin.on(predicates);
        return this;
    }

    public Predicate createEqual(Expression e1, Expression e2) {
        return cb.equal(e1, e2);
    }

    public QueryBuilder onEqual4(Expression e1, Expression e2) {
        currentJoin.on(createEqual(e1, e1));
        return this;
    }

    public QueryBuilder select5(Selection... selections) {
        cq.multiselect(selections);
        return this;
    }

    public Root getRoot() {
        return root;
    }

    public Join getJoin(String b) {
        return joinMap.get(b);
    }

    public Collection build() {
        TypedQuery query = em.createQuery(cq);
        return query.getResultList();
    }

    public Collection build(Criteria criteria) {
        Predicate predicate = criteria.toPredicate(root, cq, cb);
        if (predicate != null) {
            cq.where(predicate);
        }

        TypedQuery query = em.createQuery(cq);
        return query.getResultList();
    }

    public PageResult build(Criteria criteria, PageRequest pageRequest) {
        Predicate predicate = criteria.toPredicate(root, cq, cb);
        if (predicate != null) {
            cq.where(predicate);
        }

        TypedQuery query = em.createQuery(cq);

        query.setFirstResult(pageRequest.getPageNumber() * pageRequest.getPageSize());
        query.setMaxResults(pageRequest.getPageSize());
        Long total = this.getCountQuery(criteria).getSingleResult();

        PageResult pageResult = new PageResult();
        pageResult.setPage(pageRequest.getPageNumber());
        pageResult.setSize(pageRequest.getPageSize());
        pageResult.setTotal(total);
        pageResult.setRows(query.getResultList());
        return pageResult;
    }

    private TypedQuery<Long> getCountQuery(Criteria criteria) {
        CriteriaBuilder builder = this.em.getCriteriaBuilder();
        CriteriaQuery<Long> query = builder.createQuery(Long.class);

        Root root = this.applySpecificationToCriteria(criteria, query);
        if (query.isDistinct()) {
            query.select(builder.countDistinct(root));
        } else {
            query.select(builder.count(root));
        }

        query.orderBy(Collections.emptyList());
        return this.em.createQuery(query);
    }

    private Root applySpecificationToCriteria(Specification spec, CriteriaQuery query) {
        Root root = query.from(domainClass);
        if (spec == null) {
            return root;
        } else {
            CriteriaBuilder builder = this.em.getCriteriaBuilder();
            Predicate predicate = spec.toPredicate(root, query, builder);
            if (predicate != null) {
                query.where(predicate);
            }
            return root;
        }
    }
}
